package com.vhall.component.plugin.common.utils;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.ReturnType;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeUnit;

/**
 * redis分布式锁工具类
 *
 * @author skeleton
 */
@Slf4j
@Component
public class RedisLocker {

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    /**
     * 锁前缀Key
     */
    private static final String LOCK_PREFIX = "LOCK:";

    /**
     * 默认重试时间
     */
    private static final Integer DEFAULT_RETRY_TIME = 5000;

    /**
     * 默认过期时间
     */
    private static final Integer DEFAULT_EXPIRE_TIME = 5000;

    /**
     * 默认sleep时间 100毫秒
     */
    private static final Long DEFAULT_SLEEP_TIME = 100L;


    /**
     * 解锁的lua脚本
     */
    public static final String UNLOCK_LUA;

    static {
        UNLOCK_LUA = "if redis.call(\"get\",KEYS[1]) == ARGV[1] " +
                "then " +
                "    return redis.call(\"del\",KEYS[1]) " +
                "else " +
                "    return 0 " +
                "end ";
    }

    private static RedisLocker redisLocker;

    @PostConstruct
    public void init() {
        redisLocker = this;
        redisLocker.redisTemplate = this.redisTemplate;
    }

    /**
     * 获取锁(如果失败,自动重试)
     *
     * @param key
     * @param value
     * @return
     */
    public static void lock(String key, String value) {
        lock(key, value, DEFAULT_RETRY_TIME, DEFAULT_EXPIRE_TIME);
    }

    /**
     * 获取锁(如果失败,自动重试)
     *
     * @param key        缓存键
     * @param value      缓存值
     * @param retryTime  重试时长(毫秒)
     * @param expireTime key过期时长(毫秒)
     * @return
     */
    public static void lock(String key, String value, long retryTime, long expireTime) {
        lock(key, value, retryTime, expireTime, true, 0, 0);
    }

    /**
     * 获取锁(如果失败,直接返回false)
     *
     * @param key   缓存键
     * @param value 缓存值
     * @return
     */
    public static boolean tryLock(String key, String value) {
        return tryLock(key, value, DEFAULT_EXPIRE_TIME);
    }

    /**
     * 获取锁(如果失败,直接返回false)
     *
     * @param key        缓存键
     * @param value      缓存值
     * @param expireTime key过期时长(毫秒)
     * @return
     */
    public static boolean tryLock(String key, String value, long expireTime) {
        return lock(key, value, 0, expireTime, false, 0, 0);
    }

    /**
     * 释放锁(key对应的value于参数value一致,才删除key)
     *
     * @param key
     * @param value
     */
    public static boolean unlock(String key, String value) {
        boolean success = false;
        String fullKey = getFullKey(key);
        String cv = redisLocker.redisTemplate.opsForValue().get(fullKey);
        if (cv != null && cv.equals(value)) {
            success = redisLocker.redisTemplate.delete(fullKey);
        }
        //eval测试环境禁用
//        boolean success = unlockWithLua(fullKey, value);
        if (success) {
            log.info("解锁成功, key={}, value={}", key, value);
        } else {
            log.info("解锁失败, key={}, value={}", key, value);
        }
        return success;
    }

    /**
     * 获取锁
     *
     * @param key         key
     * @param value       value
     * @param retryTime   重试时长
     * @param expireTime  key过期时长(毫秒)
     * @param reTry       失败是否重试
     * @param curTry      重试次数
     * @param retryMillis 重试时间(毫秒)
     * @return
     */
    public static boolean lock(String key, String value, long retryTime, long expireTime,
                               boolean reTry, int curTry, long retryMillis) {
        curTry++;
        String fullKey = getFullKey(key);
        // setnx 并设置超时时间
        boolean success = setnx(fullKey, value, expireTime);
        // 获取成功,直接返回
        if (success) {
            log.debug("加锁成功; key={}, value={}, expire={}, retryMillis={}", key, value, expireTime, retryMillis);
            return true;
        }
        // 获取失败,不需要重试,直接返回
        if (!reTry) {
            log.debug("加锁失败; key={},value={}", key, value);
            return false;
        }
        // 获取失败, 且已超时,返回
        if (retryMillis > retryTime) {
            log.debug("加锁失败，已超时; key={},value={},retryTime={},reTry={}" +
                    ",curTry={},retryMillis={}", key, value, retryTime, reTry, curTry, retryMillis);
            return false;
        }
        // sleep后重新获取锁
        sleep();
        return lock(key, value, retryTime, expireTime, reTry, curTry, retryMillis);
    }

    /**
     * 解锁
     *
     * @param lockKey
     * @param requestId
     * @return
     */
    private static boolean unlockWithLua(String lockKey, String requestId) {
        RedisCallback<Boolean> callback = (connection) -> connection.eval(UNLOCK_LUA.getBytes(), ReturnType.BOOLEAN, 1,
                lockKey.getBytes(StandardCharsets.UTF_8), requestId.getBytes(StandardCharsets.UTF_8));
        return redisLocker.redisTemplate.execute(callback);
    }

    /**
     * 睡眠
     */
    private static void sleep() {
        try {
            Thread.sleep(RedisLocker.DEFAULT_SLEEP_TIME);
        } catch (InterruptedException e) {
            log.error("Interrupted!", e);
            Thread.currentThread().interrupt();
        }
    }

    /**
     * 获取加锁完整key
     *
     * @param key 键
     * @return
     */
    private static String getFullKey(String key) {
        return LOCK_PREFIX + key;
    }


    /**
     * 设置锁
     *
     * @param lockKey   键
     * @param requestId 值
     * @param expire    过期时间(毫秒)
     * @return
     */
    private static boolean setnx(String lockKey, String requestId, long expire) {
        return redisLocker.redisTemplate.opsForValue().setIfAbsent(lockKey, requestId, expire, TimeUnit.MILLISECONDS);
    }

}
